Skip to content

feat(types): derive and export the strict authoring twin of the node face - #8642

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-8345-strict-authoring-twin
Sep 8, 2026
Merged

feat(types): derive and export the strict authoring twin of the node face#8642
os-zhuang merged 4 commits into
mainfrom
claude/issue-8345-strict-authoring-twin

Conversation

@os-warren

@os-warren os-warren commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Part of #8345 — the strict face itself, under the #5250 ruling (director comment 5534418546, maintainer 2026-09-04, decision batch #25, option 2). ⛔ No consumer is wired: objectui validate, the JSON-fence gate and objectui check are devx's half and are ruled to come after.

Attribution, in prose because a body edit downgrades the footer: written by Claude Code, seat session session_01Jmxdo7bmeqCQHLSfmLVX9w.

Notation. Generic parameters and placeholders are written as UPPERCASE words rather than in their real angle-bracket spelling, because GitHub deletes tag-shaped fragments on save — backticks and fences included.


Patch round at 8e9b563 — the contract review returned FAIL, and it was right

The ceiling-tier review (verdict 5591195581) found a real defect underneath every passing pin. It is fixed here, and the two prerequisites it named are now pinned. The fresh whole-change review of this head (5592039737) returned PASS.

The defect

The walker's type guard was typeof value === 'object'. Zod 4.4.3 builds some objects through $ZodObjectJIT, and those instances are CALLABLE — they answer typeof 'function', their constructor prints as a bound ZodObject, their traits read ZodObject/$ZodObjectJIT/$ZodObject/$ZodType, and they parse like any other object. 20 of them are reachable on this face, all through @objectstack/spec-derived subtrees. The guard handed each one straight back, so the whole subtree beneath went unwalked.

⚠️ And they are not only objects: the review's independent walk classifies the 20 as 15 objects · 3 enums · 1 record · 1 pipe. The fixed guard admits all of them.

Measured on the built dist, before and after, by the same probe:

reading at ad99bee at 8e9b563
function-typed nodes on the tolerant face 20 20
objects on the forced strict twin 302 300
objects still OPEN on the twin 6 0
REPRO-A — invented key at page.interfaceConfig.sort[] strict accepted, key silently dropped strict refused, unrecognized_keys names inventedDeepKey
control, same document, invented key at the root refused and named refused and named
tolerant face on REPRO-A accepted accepted
pipes reachable on the face 1 4

The six that were open: page.interfaceConfig.sort[], page.slots.header[0].in.visibleWhen[1] and its .meta, page.slots.header[0].in.dataSource.filter(lazy).right (all zod strip mode), and list-view.bulkActionDefs[].params[] with …params[].options[] (both catchall: unknown, i.e. passthrough).

⛔ The claim was not weakened to match the walker. The walker was fixed so the published sentences — in the changeset, the README and this body — became true. All three are unchanged.

⭐ Why nothing here could have caught it, and the pin that now does

Two instruments shared the defect with the thing they were measuring.

  1. The corpus is structurally blind. No document among the 556 carries an undeclared key inside those six objects. Re-derived after the fix: whole-tree strict is 174 / 556, red-today 46, base equals head in every cell, and the shipped-face agreement check still reads 174 / 556identical to the pre-fix run. ⇒ ⛔ The corpus is not evidence this fix worked. REPRO-A and the population pin are. The corpus is reported below only as the unchanged-accept-set statement it was always good for.
  2. The pin file's own census began typeof node !== 'object' — the identical blind spot. That is why "39 closed, identical before and after" read clean.

⇒ The new pin is over the population, not over a sample document: walk the forced twin and require every object in it to carry catchall: never. Its non-vacuity control is the one whose absence let this through — functionTyped must be greater than zero, so a census that cannot see callable nodes fails loudly instead of reporting a graph it never entered.

⚠️ Precision on ablation leg F, corrected by the review and worth stating exactly. With the walker CORRECT, blinding only the census still leaves it seeing the twin's object-typed clones, so the population assertion is green because it is true — F's green is not vacuous, and only the control fires. The genuinely vacuous case is E+F together: walker blind and census blind, which reconstructs the exact state that shipped at ad99bee. There the population assertion goes green with nothing behind it, and the callable-node control is the only thing that fires. That leg is the proof the round-1 gap is closed, and I had described my own pin as slightly stronger than it is.

The cycle invariant, now pinned

The review measured the module cycle under four bundlers. Node, Vite/rolldown and Next 16.3.1 Turbopack are green in both entry orders. rollup 4.62.2, deep-module-first with a namespace import used as a value, is REDReferenceError: Cannot access 'StrictAnyComponentSchema' before initialization, from the namespace object rollup synthesizes ahead of the deep module's body; re-confirmed still red at this head. Named imports in the same order are green; barrel-first is green.

Unreachable today, and that is the point: my stated reason for safety — "read only inside the lazy getters" — is true and not sufficient. The load-bearing invariant is the barrel is the sole entry into the cycle, and it now has a pin covering both routes a caller has:

  • the published exports map has no wildcard subpath and no entry reaching the deep module (so nothing outside the package can deep-link it);
  • no module anywhere in this repository imports the deep module except the barrel — a scan of 4,523 source files under packages, apps, examples, scripts, e2e, matched on import specifiers so that scripts/measure-strict-authoring-face.mjs does not read as an importer merely by sharing the words in its own name.

⚠️ The vitest alias table maps @object-ui/types to packages/types/src by prefix, so inside this repo a deep specifier resolves even though the exports map blocks it for consumers. The repo scan is the half that covers that route; the manifest assertion alone would not.

The opaque-limit census — four numbers, and which one is the face

⚠️ This body previously said 79. That figure was wrong, and wrong in an instructive way: it was a census of one run rather than of the face. A count of this kind is meaningless without the method that produced it, which is how the same class of error was made twice on this card. So all four readings are given, each with its method, re-derived at 8e9b563 with an every-def-key walk written for this purpose rather than reusing the shipped walker:

reading method figure
the deriver, no forcing onOpaqueShape over the eager walk alone; every z.lazy still deferred custom 70 · function 4 · transform 4 = 78
the deriver, one document parse eager walk plus whatever that one parse happened to force — the old, wrong figure custom 71 · function 4 · transform 4 = 79
the deriver, every lazy forced what the walker reports about the whole face — the figure this body carries custom 72 · function 4 · transform 4 = 80
the face itself, every def key an independent walk that enumerates every schema-bearing def key, not a fixed list custom 74 · function 4 · transform 4 = 82

80 is the census of the face as the walker sees it, and it agrees exactly with the review's independent reading.

⭐ The last row is worth keeping rather than rounding away. The two extra custom nodes are reached only through def.checks — they live inside a refinement, not in a schema's shape. The walker never traverses checks, deliberately: checks are carried across whole by the def-patching clone rather than rebuilt, which is the property the refinement pins exist for. So onOpaqueShape's contract is precisely "every unclosed shape the walker meets", ⛔ not "every opaque node on the face" — a distinction with no accept-set consequence (a preserved check still runs) but one this body should state rather than let a reader infer the stronger claim.

Pipes. Four reachable, not one: transform into enum (a preprocessor under page.interfaceConfig.filterBy[]), plus object, string and array each into a transform. The walker's own comment said "one pipe" — a reading taken through the blind guard — and commit 8e9b563 rewrites it. No OBJECT sits on an out side today, so walking both sides still moves no accept set; it is there so the first preprocessor wrapping an object does not open a hole.


What lands

Three values and two types, published from @object-ui/types/zod:

export what it is
StrictAnyComponentSchema the document-root twin of AnyComponentSchema
StrictSchemaNodeSchema the child-slot twin of SchemaNodeSchema
deriveStrictAuthoringSchema(schema, options) the derivation, so no consumer writes a second walker
StrictAuthoringLimit, DeriveStrictAuthoringOptions types for the onOpaqueShape report

Derived, never hand-written. Every reachable object is closed through unions, discriminated unions, arrays, tuples, records, intersections, optionals, nullables, defaults, both sides of a pipe, z.lazy, and — since the patch round — callable JIT nodes. Objects are cloned by patching a copy of their own def and calling their own constructor — ⛔ never rebuilt with a fresh object literal, which drops def.checks and would make the twin under-report red. A callable node clones through its own bound constructor and comes back object-typed: a difference in representation, not in behaviour, and the population pin measures behaviour.

The rendering face's passthrough is not flipped. This adds a face; it does not change the existing one — pinned, not promised.

⚠️ Re-derived; ⛔ no figure is inherited from the card body

All at 8e9b563, zod 4.4.3, corpus 556 documents / 2099 nodes (catalog + docs + authored). The card's 176/553 and 184/2099 are pinned at 5505aec1 and are not these numbers.

reading at c4326fe (base) at head
documents refused, whole-tree strict 174 / 556 174 / 556
documents refused by the face as shipped 46 / 556 46 / 556
nodes refused under strict 179 / 2099 179 / 2099
— of those, green today, red only under strict 127 127
— already red under the face as shipped 52 52
component types declared / seen / strict-clean 107 / 94 / 62 107 / 94 / 62
registry collisions · arms with no literal type 0 · 0 0 · 0

Base and head are identical in every cell — the corpus-scale statement that this PR moves nothing about the existing face. ⚠️ And the same table was identical before the guard fix, which is exactly why it cannot stand as evidence that the face is closed.

Agreement with the prototype, with the script's own whole-tree twin swapped for the shipped StrictAnyComponentSchema (scratch copy, never committed): 174 / 556, red-today control 46 — identical.

The blocker's property, re-read on this tree

The old 294/553 blow-up is gone: whole-tree strict (174) now sits beside per-node strict (179 of 2099) instead of an order of magnitude above it, because since #8344 a child slot resolves to the component union rather than the base keys. That is the error #7935 existed to prevent.

Where the module lives — ruled A by the seat (5590686191), not reopened

__tests__/zod-mirror-parity.test.ts runs a census closed over the export consts of src/zod/*.zod.ts, and that file is fenced this round (PR #8553). So the derivation module lives at packages/types/src/strict-authoring-face.ts, outside the mirror directory, and the barrel re-exports from it. The collision is measured, not predicted — ablation leg 1 below. B (move it in, add an EXCLUSIONS row) is recorded as a follow-up for after #8553 lands.

The pins

packages/types/src/__tests__/strict-authoring-face-8345.test.ts25 tests.

  • (a) a known-good card with button and text children parses under the strict face, and under the tolerant one.
  • (b) one invented top-level key ⇒ refused, unrecognized_keys naming exactly that key; the same document accepted by the tolerant face.
  • (c) the tolerant face is unchanged: a five-row verdict table read after the strict derivation is forced, plus a non-mutation reading (closed-object count and open-path count both identical before and after a twin is built and forced).
  • the population is closed — every object reachable on the twin carries catchall: never; plus the callable-node control; plus a discriminator control showing the tolerant face is not closed.
  • REPRO-A — an invented key deep inside a spec-derived subtree is refused and named, with the root-level control in the same document.
  • the recursion point — an invented key on a CHILD is refused and named; a child's own variant / size are ACCEPTED.
  • checks survive the clone — synthetic refinement still fires, with the rebuild caricature as a control; and the live chatbot body clause still refuses a record body one slot down while the root form is accepted, on both faces.
  • the limits are enumerated, not claimed — every reported kind is one of the three recorded opaque kinds, the census is non-vacuous, and a synthetic control pins that the reporter recognises all three; plus the preprocessor pin for the out side of a pipe.
  • the barrel is the sole entry into the cycle — the two halves above.

⚠️ The type-level pins are read by tsc -p tsconfig.test.json and by nothing else — vitest does not typecheck. Non-vacuity with --listFiles: the test program lists the pin file (1 hit), the emitting program does not (0), both list the source.

Ablations — each proved on disk, each restored under a trap

Every leg: blob hash before and after (a byte-identical mutation aborts the leg as a no-op), an EXIT INT TERM trap with absolute paths, and a restore verified by the hash returning to the HEAD blob and git diff HEAD being empty. Every leg printed its RESTORED-OK line.

leg mutation instrument result
1 · census collision one export const appended to zod/index.zod.ts vitest, parity file 1 failed / 31 passed, naming index.zod.ts#__StrictAuthoringCensusProbe
2 · rebuild caricature object clone replaced by a rebuilt object literal vitest EXIT 1 — the refinement pin
3 · no out walk the pipe's out branch deleted vitest EXIT 1 — 2 failed
4 · deriver return type widened generic return replaced by the base schema type tsc -p tsconfig.test.json EXIT 1 — TS2741 at the type pin
5 · the same mutation (as above) vitest EXIT 0, 19 passed — vitest is blind
E · the guard regressed walker guard back to object-only vitest EXIT 1 — 2 failed: the population pin, expected [ …(6) ] to deeply equal [], and REPRO-A
F · the census blinded the PIN FILE's census back to object-only, walker left correct vitest EXIT 1 — only the callable-node control fires; the population assertion is green because true (see the precision note above)
E+F · both blind the exact state that shipped at ad99bee vitest EXIT 1 — callable control + REPRO-A, and the population assertion green vacuously. ⭐ The leg that proves the new control does what the round-1 instrument could not
G · manifest assertion the deep-module name pointed at one the exports map does reach vitest EXIT 1 — the exports-map half
H · a new deep importer a file added under packages/core/src importing the deep module vitest EXIT 1 — the sole-entry scan; restore verified by git status --porcelain empty

Gates

All at 8e9b563; each exit code captured before any pipe.

gate exit
pnpm exec vitest run packages/types/ 0 — 152 files, 2902 tests
pnpm --filter @object-ui/types type-check (all three programs) 0
pnpm --filter @object-ui/types lint 0 — 0 errors, pre-existing warnings only, none in the changed files
pnpm --filter @object-ui/types build 0 — 126 emitted files
check:control-bytes · check:published-tsconfig-exclude · check:side-effects-array · check:esm-specifiers · check:self-import 0
check:dist-completeness · check:published-dist 0
check:doc-fences · check:doc-snippets · check:doc-types · check:doc-examples 0
check:governed-queue-guard 0; --test on all five changed paths: NOT GOVERNED
node scripts/check-changeset-presence.mjs 0
targeted grep -naP control-byte self-scan of the changed files no match
check:node-esm-load, plain 1 — RED
check:node-esm-load --force-build 0 — GREEN, 37 of 37 … built by this tree, 34 of 39 … imported and evaluated, 5 by design
CI on this head 33 check runs, all completed, zero failures

⚠️ Correction to what I wrote about that gate. The provenance root-cause stands: turbo shares one .turbo/cache across every worktree of this checkout, so the plain run refuses two entries it did not build, neither of them in this diff, while @object-ui/types is built by this tree in both runs. But my sentence "on CI the refusal cannot arise" implied a coverage CI does not provide. node-esm-load-gate.yml runs on a nightly cron and on push to main; only check:esm-specifiers runs per PR. ⇒ the load leg is unmeasured by anyone on a PR head — ⛔ not "left to CI".

⚠️ Correction to what I wrote about the carrier label. I reported it as "something else wrote the label set whole". That was an inferred mechanism, ⛔ not a reading, and the event log falsifies it: a discrete unlabeled event by a named account, on the card and the PR two seconds apart — a dual clear, which is the shape of a deliberate clear rather than a whole-set overwrite side effect. The correct instrument was one call away. Carrier state is the PM seat's to hold; ⛔ I do not touch labels.

Not measured, stated rather than implied: the repo-wide turbo run lint and the full pnpm test beyond CI's green run on this head; esbuild/tsup bundling of the cycle; any browser or runtime rendering (nothing renders this face); import-time cost in a bundled app (the derivation is deferred behind z.lazy, so it is not paid unless the face is parsed).

Scope

Changeset: @object-ui/types minor — a new published face on a published entry point; grade confirmed against AGENTS.md 238–240 and the precedents. Five files: the derivation module, the barrel re-export block, the pin file, the package README, the changeset. ⛔ No fenced file is touched: not packages/types/package.json (the cycle pin reads it, never writes it), not the package's build configuration, not src/record-components.ts, not __tests__/zod-mirror-parity.test.ts.

Falls off the back, as ruled: wiring any consumer; repairing any of the 174; the TypeScript authoring face (#7927); closing the opaque validators.

Clause ② carrier: held and cleared by the PM seat on the verified PASS for this head. ⛔ Not mine to set, clear or reason about here. This PR stays draft, ⛔ not enqueued, ⛔ no auto-merge, until that seat says otherwise.

…face

Publish `StrictAnyComponentSchema`, `StrictSchemaNodeSchema` and
`deriveStrictAuthoringSchema` from `@object-ui/types/zod` — a derived twin of
the node face that closes every declared object at every depth.

Additive: the rendering face keeps its `.passthrough()`, no accept set moves,
and no consumer is wired to the new face.

The twins are derived, never hand-written. Objects are cloned by patching a
copy of their own def so `.refine()` / `.superRefine()` checks survive; a twin
rebuilt with `z.object(shape)` would drop them and under-report red. Both sides
of a pipe are walked, so a `z.preprocess`'s real schema is closed too.

Part of #8345

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
The snippet used `ButtonSchema` without importing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types tests labels Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.4 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-CUuu9hOC.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 243.73KB 61.53KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 251.10KB 64.96KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 11.72KB 4.48KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…e population

The walker's type guard was `typeof value === 'object'`. Zod 4.4.3 builds some
objects through `$ZodObjectJIT`, whose instances are CALLABLE — 20 of them are
reachable on this face, all through `@objectstack/spec`-derived subtrees. The
guard handed each one back untouched, so the subtree beneath went unwalked and
6 objects stayed open on the twin: an invented key inside one of them was
accepted and silently dropped, while the same key at the root was refused and
named.

Nothing could catch that. No document in the 556-document corpus carries an
undeclared key inside those 6 objects, and the pin file's own census began
`typeof node !== 'object'` — it shared the blind spot with what it measured.

Both guards now admit callable nodes, and the pin that was missing is here: a
POPULATION pin requiring every object reachable on the twin to carry
`catchall: never`, with the function-typed count asserted non-zero so the
control cannot silently regress. REPRO-A is pinned as a document too, and the
cycle's real invariant — the barrel is the sole entry — is pinned in both of
its halves.

Part of #8345

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #8642 @ ad99bee

Verdict: FAIL — one blocking defect in the exported face, independently re-measured on this head (not inherited from the card thread). The three owed pins hold; the published contract text does not.

Ruling implemented: objectui#5250, director comment 5534418546, provenance maintainer, live PM chat, 2026-09-04, decision batch #25, option 2. Operative clause, verbatim: "each node schema gets a derived strict variant; objectui validate and the doc-snippet gates run strict; renderer props keep the tolerant face unchanged", with "⛔ The rendering face's .passthrough() is not flipped." Card #8345 (priced on #7935; blocked on #8344, landed as PR #8501 841dd2b7) scopes this PR to the derivation + export + pins (a)(b)(c), no consumer wired. Scope as ruled: the PR adds a face, wires nothing, flips nothing — confirmed by diff (base.zod.ts:417 still .passthrough(); src/zod/ differs from merge-base c4326fe only in the barrel's appended re-export block).

Governed paths touched: NO (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**, content/docs/releases/** all absent from the 5-file diff). packages/types/package.json untouched; "sideEffects": false intact; exports has ./zod and no wildcard — the deep module is unreachable by specifier.
Clause-②: yes (new export) — three values + two types on the published @object-ui/types/zod entry; mechanical floor. Carriers: needs:contract-review on PR and on card — present on both.

Readings (own worktree-free build: git archive of ad99bee into scratch, sibling-install toolchain zod 4.4.3 / spec 17.3.0, tsc -p tsconfig.json exit 0 → 88 dist files incl. strict-authoring-face.{js,d.ts}; tsc -p tsconfig.test.json exit 0)

reading value
pins (a) known-good / (b) root key refused + named / (b) tolerant accepts / (c) 5-row tolerant table after forcing true / false + ["inventedTop"] / true / all 5 rows unchanged
child slot (recursion point live): invented child key refused + named; child's own variant/size accepted ["inventedChild"]; true — R4 shape, not R2
StrictSchemaNodeSchema: bare string accepted / text + nope refused true / false
corpus (scripts/measure-strict-authoring-face.mjs --json against my scratch dist) 556 docs · 174 whole-tree strict · 46 red today · 2099 nodes · 179 refused (127 strict-only / 52 red) · 107 / 94 / 62 · collisions 0 · arms w/o literal type 0 — identical to every cell the PR quotes
plausibility vs card tables card R4 = 176/553 @ 5505aec, R2 = 294/553; this head 174/556 sits on the R4 line, the R2 blow-up is gone. Controls present (red-today, collisions, arms-without-literal).
opaque census (onOpaqueShape, one forced parse) custom 71 · function 4 · transform 1 = 76
ZodError on the strict path (#8498 cliff, closed by #8544) one invented leaf key at depth 0/1/2/4/6 → flat issues 1/12/23/45/67, JSON 111 B → 9,177 B, ≤0.5 ms/parse — linear, not the 25×/level cliff; cloneWithDef preserves the discriminator, so #8544's fix carries into the twin
tolerant face non-mutation (function-aware census) closed objects reachable from AnyComponentSchema: 58 before and after forcing both twins
module cycle, Node on dist deep-module-first: same object as barrel's, nested key refused; barrel-first: same object, export present. No load-time derivation (createStrictWalker() = one empty Map, two z.lazy wrappers). New edges: strict-authoring-face.tszod/index.zod.ts only — no edge from base.zod.ts (the refused getter-binding route is not taken; base.zod.ts is byte-identical to base).

Findings

1. BLOCKER — the strict twin does not close every reachable object; 6 stay open, and an off-spec key inside them is silently accepted and dropped.
src/strict-authoring-face.ts:141-142: isZodType = (value) => typeof value === 'object' && value !== null && '_zod' in value. On this face 20 zod 4.4.3 schema nodes are typeof 'function' (def kinds: object 15, enum 3, record 1, pipe 1; they parse normally). The guard returns each one unwalked, so its whole subtree keeps the tolerant shape. Function-aware census of the forced StrictAnyComponentSchema: 302 objects, 296 closed, 6 openpage.interfaceConfig.sort[] (strip), page.slots.header[0].in.visibleWhen[1] + its .meta (strip), page.slots.header[0].in.dataSource.filter(lazy).right (strip), list-view.bulkActionDefs[].params[] and …params[].options[] (catchall: unknown, i.e. passthrough).
Reproducer: { type:'page', interfaceConfig:{ source:'x', sort:[{ field:'a', order:'asc', inventedDeepKey:1 }] } } → tolerant true, strict true, no unrecognized_keys, strict output {"field":"a","order":"asc"} (key dropped). Control, same document + inventedTopKey at root → strict false, ["inventedTopKey"] named.
Fix ablation (scratch copy, guard admits typeof value === 'function'): 300 objects / 300 closed / 0 open; reproducer → strict false, ["inventedDeepKey"]; known-good still true; list-view.…params[0].bogus now named alongside the pre-existing required-value issues. The prototype's guard (if (!schema?._zod) return schema;, script line 260) does not have this hole — the shipped walker regressed coverage relative to the prototype on exactly this class.
Expectation: admit function-typed nodes in isZodType; add a pin that walks the forced twin with a function-aware visitor and asserts every reachable object def has catchall.type === 'never' (with a non-vacuity assertion that ≥1 function-typed node was visited); add the reproducer above as a pin, both directions.

2. MAJOR — the published contract text is false at those six depths. Changeset: "refusing any undeclared key at any depth with an unrecognized_keys issue that names it"; README: "closes every declared object, at every depth"; PR body: "Every reachable object is closed". The changeset is the payload that ships. Expectation: make the text true (finding 1) — do not soften the text instead.

3. MINOR (latent) — the walker's default: arm swallows container def kinds silently and reports nothing. Synthetic probe: z.set(obj), z.map(k, obj), obj.prefault(…) → strict twin accepts {a:'x', bogus:1} inside them, inner objects 0 closed, and no onOpaqueShape call — so the "complete limit list = custom/function/transform" claim holds only for kinds the switch names. None of these kinds is on the face today (after the finding-1 fix the census reads 0 open), so no accept set moves now. Controls: catchall: obj and z.record(k, obj) are walked correctly (strict refuses). Expectation: add set/map/prefault/promise arms, or make default: report unknown non-leaf kinds through onOpaqueShape so the limit list is measured rather than enumerated by hand.

4. MINOR — the pin file's instrument shares the defect. closedObjectCount (test file, bottom) starts with typeof node !== 'object', so the "identical before and after" reading and the "39 closed" figure could not see the six open objects; the corpus is blind too (no document among the 556 carries an undeclared key inside those objects — base, head and the swapped-in shipped face all read 174 regardless). Expectation: function-aware counter; the pin in finding 1 is the control the file is missing.

5. INFO — the module cycle's real invariant is "the barrel is the sole entry", and nothing pins it. "Read only inside the lazy getters" is true of the shipped source (import at line 84, runtime reads only at 300/312) and is what makes both Node orders green — but rollup's synthesized namespace on a deep-first entry is the shape #8344 paid for, and the only thing keeping it unreachable is the exports map plus the fact that no file but the barrel imports strict-authoring-face. Build Docs on this head is the skip path (12 s, "No docs-related files changed") and says nothing about Turbopack. Expectation (non-blocking): a source-scan pin that no module other than zod/index.zod.ts imports strict-authoring-face.

6. INFO — check:node-esm-load is not a per-PR gate. node-esm-load-gate.yml triggers on schedule (17 4 * * *) and on push for its own two paths; only check:esm-specifiers runs per PR. So "cannot arise on CI" is true only because CI never runs the load leg on a PR. The provenance explanation is plausible and not this diff, but the PR should state the --force-build re-read rather than lean on CI.

7. INFO — placement (option A) and the census collision, reproduced. Unmutated: pin file 19/19 and parity file 32/32 under vitest. Appending one export const to zod/index.zod.tsTests 1 failed | 31 passed (32), failure naming index.zod.ts#__StrictAuthoringCensusProbe. zod-mirror-parity.test.ts is unchanged vs base (fence honoured); the zod-lazy-getter-identity-7918 row lives in its own test file, also unchanged. Seat ruled A on the card; nothing here is a maintainer item.

8. INFO — the claim comment on #8345 (5590024076) carries no Container & model: line. Tier stated: none. (The dispatch comment says "built at TIER_DEFAULT"; the claim itself does not carry the line.) Commit trailers: both commits carry Co-Authored-By + Claude-Session; commit 1 carries Part of #8345. RULE 2: satisfied.

Changeset verdict

.changeset/8345-strict-authoring-face.md: '@object-ui/types': minorgrade correct (additive export, nothing removed, no BREAKING). Text false at the six depths in finding 1; must be corrected with the fix, not reworded around it.

Tests

19 pins pass; no .skip/.only/.todo; both directions pinned (strict refuses root and child invented keys and names them; tolerant control accepts the same documents; five-row tolerant table after forcing); revert reddens (the exports do not exist at base). What is missing is the whole-graph closure pin (finding 1/4).

CI on ad99bee

33 check runs, all completed, none in progress: Type Check ✓ (19:33:19) · Build Docs ✓ (skip path, 12 s) · Bundle Analysis ✓ (types (strict-authoring-face.js) 11.72 KB / 4.48 KB gz; console eager closure 3477.4 KB under the 3512.7 KB budget) · Test (shard 1/4 … 4/4) all ✓ · Lint ✓ · Changeset Bump Policy / Declaration / Fixed Group / Overwrite ✓ · README Export Check ✓ · Governed Surface Queue Guard ✓. mergeable_state: unknown at read (draft; 8 commits behind origin/main 4dc80d0f).

Maintainer-only merge: no — non-governed additive export under a maintainer-ruled programme; every finding above is implementer-floor. Carrier needs:contract-review stays on PR and card until finding 1 is fixed and re-reviewed.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-DqTtGmW_.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 243.73KB 61.53KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 13.66KB 5.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

The `pipe` arm's comment said "one pipe reachable, `in` an array and `out` the
transform". That reading was taken through the object-only guard. With callable
nodes admitted, four pipes are reachable — including a preprocessor under
`page.interfaceConfig.filterBy[]` that the old walker could not see at all.

No code changes; the comment now states what the tree measures.

Part of #8345

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-DqTtGmW_.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 243.73KB 61.53KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #8642 @ 8e9b563d80742bfd297d5fc822ae05d63a8fcf5f

Verdict: PASS — the blocking defect from the ad99bee review is fixed at the walker and pinned over the population, re-measured here from my own build of this head (not inherited from the PR body or the card). Two owed MINOR/INFO items are not met (table below); neither falsifies a published sentence. One MINOR is recorded as an open latent gap for the PM seat to route (follow-up or small pre-merge patch); two are carrier/claim hygiene items outside the diff.

Ruling implemented: objectui#5250, director comment 5534418546, maintainer 2026-09-04, decision batch #25, option 2 ("each node schema gets a derived strict variant; … renderer props keep the tolerant face unchanged"; "⛔ The rendering face's .passthrough() is not flipped"). Card #8345 scopes this PR to derivation + export + pins (a)(b)(c), no consumer wired.

Governed paths touched: NOscripts/check-governed-queue-guard.mjs --test on all 5 changed paths: NOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched. Fenced files untouched: git diff --stat origin/main...head -- packages/types/package.json packages/types/src/record-components.ts packages/types/src/__tests__/zod-mirror-parity.test.ts is empty; src/zod/** differs from merge-base c4326fe only in the barrel's appended re-export block (byte-diff read). base.zod.ts:417 still .passthrough().

Clause-②: yes (new export). Three values + two types land on the published @object-ui/types/zod entry (StrictAnyComponentSchema, StrictSchemaNodeSchema, deriveStrictAuthoringSchema, StrictAuthoringLimit, DeriveStrictAuthoringOptions); a consumer can observe them, so the mechanical floor applies regardless of whether any existing accept set moved — and the tolerant face's accept set did not move (corpus base = head in every cell, non-mutation pin green). Changeset '@object-ui/types': minor, no major, grade correct for an additive export; .changeset/8345-strict-authoring-face.md present and unchanged since ad99bee.

What moved ad99bee → 8e9b563 (2 commits, 2 files, +231/−25)

packages/types/src/strict-authoring-face.ts (+51: isZodType admits typeof 'function'; pipe-arm comment re-derived) and packages/types/src/__tests__/strict-authoring-face-8345.test.ts (+205: closedObjectCount → function-aware census(), 6 new pins: population closed, callable-node control, tolerant-face discriminator, REPRO-A, exports-map half, sole-importer scan). README, changeset, barrel: byte-identical to ad99bee.

Readings (own build: git archive of head into scratch, sibling install read-only via symlinks — zod 4.4.3, spec 17.3.0; tsc -p tsconfig.json exit 0 → 88 dist files; tsc -p tsconfig.test.json exit 0, pin file in the test program --listFiles 1 hit, emit program 0 hits; vitest pin file 25/25 green, no .skip/.only/.todo)

reading value
tolerant face, function-aware census (forced) 4766 nodes · 20 callable (object 15, enum 3, record 1, pipe 1) · 300 objects · 58 closed / 242 open
strict twin, same census 4998 nodes · 300 objects · 300 closed / 0 open
REPRO-A {type:'page', interfaceConfig:{source:'x', sort:[{field:'a', order:'asc', inventedDeepKey:1}]}} tolerant true · strict false, unrecognized_keys = ["inventedDeepKey"]
control: same doc + inventedTopKey at root strict false, ["inventedDeepKey","inventedTopKey"]
control: same doc without the key strict true
known-good card/button/text; root invented key strict true; strict false ["inventedTopLevelKey"]
list-view.bulkActionDefs[].params[] / …options[] with a bogus key strict names bogusParamKey / bogusOptionKey (alongside pre-existing issues; these two were catchall: unknown at ad99bee)
opaque census (onOpaqueShape, one forced parse) custom 71 · function 4 · transform 4 = 79
pipes reachable on the face 4: transform→enum (…interfaceConfig/…/filterBy/…/element/operator, the preprocessor), object→transform (…slots/…/header/…/opt0), string→transform (…header/…/in/visibleWhen/…), array→transform (…exportOptions/…)
guard ablation on a dist copy (object-only guard, walker otherwise head) 302 objects · 296 closed · 6 open…interfaceConfig/…/sort/…/element [strip], …header/…/in/visibleWhen/…/opt1 [strip], its …/meta/innerType [strip], …in/dataSource/…/filter/…/lazy/right [strip], …bulkActionDefs/…/params/…/element [unknown], …/options/…/element [unknown]; REPRO-A strict true, no keys; opaque back to 76
ablation E under vitest (src guard reverted, blob 269c59b6→ecf52cc1, restored to 269c59b6) exit 1, 2 failed / 23 passed: population pin expected [ …(6) ] to deeply equal [] + REPRO-A
ablation F under vitest (pin census blinded, walker correct, blob 4f24ec93→d2ab2853, restored) exit 1, 1 failed / 24 passed: only "the census can see CALLABLE schema nodes" fires (expected 0 to be greater than 0); population assertion vacuously green — the control is doing its job
corpus scripts/measure-strict-authoring-face.mjs --json, base c4326fe vs head 556 / 174 / 46 / 2099 / 179 = 127+52 / 107·94·62 / 0·0 — identical in every cell; whole JSON identical except the scratch path inside one error string; script itself unchanged by the PR
cycle invariant, measured directly exports map: 0 wildcard subpaths, 0 entries reaching strict-authoring-face; repo specifier scan (packages apps examples scripts e2e, excl. node_modules/dist): sole importer packages/types/src/zod/index.zod.ts
default: arm probe (owed item 3) z.set(obj), z.map(k,obj), obj.prefault(…), z.promise(obj) → inner object 0/1 closed, strict twin accepts {a:'x', bogus:1}, 0 onOpaqueShape calls; controls z.record(k,obj) and .catchall(obj) → 1/1 closed, refused

Findings

1. RESOLVED (was BLOCKER) — the twin closes every object reachable on the face. strict-authoring-face.ts:169 guard now value !== null && (typeof value === 'object' || typeof value === 'function') && '_zod' in value. Population 300/300 on my build; REPRO-A refused and named; root control unchanged; guard ablation reproduces the six open objects and the silent accept exactly; ablation E turns the new population pin and REPRO-A red. The callable nodes clone back object-typed (strict census functionTyped = 3, all enum leaves), which is representation, not behaviour — the pins read behaviour.

2. RESOLVED (was MAJOR) — changeset / README / body text is true as measured. "refusing any undeclared key at any depth", "closes every declared object, at every depth", "Every reachable object is closed": 0 open objects on the reachable graph. Text kept, not softened — the direction the previous review asked for. (The director's optional "cite the 300/300 census in the changeset" was not taken; keeping a drifting number out of the shipped payload is defensible, and the population pin is where the number lives.)

3. MINOR (latent) — NOT MET, and not acknowledged in the body or the os-dev-report: the default: arm still swallows set / map / prefault / promise. strict-authoring-face.ts:296-300 (default: out = schema; with no report). Measured above: inner objects unclosed and unreported, so for a consumer calling deriveStrictAuthoringSchema on a schema carrying one of these, "reported through onOpaqueShape rather than skipped" does not hold. No accept set moves today (none of these kinds is on the face: the tolerant census shows 0 such nodes, and the population pin's census() does descend valueType/innerType, so the first such container to reach the face with an object inside would turn the population pin red — the face is guarded; consumer-derived schemas are not). No pin covers the report path for these kinds (grep -nE "z\.set\(|z\.map\(|prefault|z\.promise" on the pin file: 0 hits). Expectation: add the four arms (set: valueType; map: keyType+valueType; promise/prefault: innerType) or make default: report unknown non-leaf kinds via onOpaqueShape; extend the three-kind control pin. Small enough to land here while the PR is draft; proportionate as a follow-up if the PM seat prefers — the director's re-hang precondition named items 1–4, and this one was re-hung short.

4. RESOLVED (was MINOR) — the pin's instrument admits callable nodes. isSchemaNode at the bottom of the pin file mirrors the walker's guard; functionTyped asserted > 0; ablation F shows that control is the only thing standing between a blinded census and a vacuous green.

5. RESOLVED (INFO) — cycle invariant pinned in both halves; false evidence sentences gone. Exports-map half and sole-importer scan are in the pin file (predicates re-measured independently above; the G/H mutations were not re-run by me). PR body: "cannot arise on CI" appears only inside a correction paragraph that retracts it; Build Docs green is not cited (on this head it is again the 12 s skip path, 20:40:40→20:40:52, and says nothing); check:node-esm-load --force-build is stated as the only reading of the load leg. The rollup deep-first RED is recorded as the reason the invariant matters.

6. INFO — NOT MET: the re-hang comments on #8345 (5591600708, 5591636881) carry no Container & model: line. Tier is fused from harness stamps by the seat; this is claim hygiene only. Commit trailers on both new commits: Part of #8345, Co-Authored-By, Claude-Session present.

7. INFO — NOT MET at read time: needs:contract-review is on the PR but NOT on the card. PR labels at read: documentation · package: types · tests · needs:contract-review. Card #8345 labels at read (GraphQL get_labels, 5 total): package: types · pm:dispatched · priority:p2 · pm:blocking · domain:spec — carrier absent; card updated_at 20:46:15Z, the same instant as comment 5591636881, which states "carrier present on both PR and card". That sentence does not match the label set I read. ⛔ This seat touches no labels; the PM seat decides the card side.

8. INFO — the corpus is confirmed structurally blind to finding 1, from the other side. Base = head in every cell and the guard ablation moves no cell either (the pipe/opaque counts move, the corpus does not). The body says so; recorded here so nobody cites the corpus as evidence of closure.

Owed item → status

# owed item (director 5591397819) status evidence
1 BLOCKER: guard rejects callable nodes → 6 open; fix + reproducer pin met 300/300 closed on my build; REPRO-A refused, inventedDeepKey named; dist ablation → 302/296/6 + silent accept; ablation E → 2 failed
2 MAJOR: "every reachable object" text false at those depths met text unchanged, now true at 0 open; census not cited in changeset (optional half not taken)
3 MINOR: default: swallows set/map/prefault/promise — walk or report, extend control pin not met probe: 0/1 closed, accepts bogus, 0 reports; no arm added, no pin, not mentioned in body or report
4 MINOR: pin's own counter blind met isSchemaNode admits functions; functionTyped > 0 control; ablation F fires only that control
5 INFO: pin "barrel is sole entry"; drop Build Docs / "cannot arise on CI" met two pins present; exports map 0 wildcard / 0 deep entry; sole importer = barrel; body corrected
6 INFO: Container & model: on the re-hang comment not met absent from 5591600708 and 5591636881
7 re-hang needs:contract-review on both PR and card partly met PR: present. Card: absent at read (5 labels, none is the carrier)

CI on 8e9b563 (read once, 21:0x Z)

33 check runs, all completed, 0 failures: 30 success, 3 skipped (Test (coverage), Test (coverage shard …), dependabot). Type Check ✓ · Test shards 1–4 ✓ · Lint ✓ · Bundle Analysis ✓ (types (strict-authoring-face.js) 14.27 KB / 5.47 KB gz; console eager closure 3477.5 KB under 3512.7 KB) · Changeset Bump Policy / Declaration / Fixed Group / Overwrite ✓ · Governed Surface Queue Guard ✓ · README Export Check ✓ · Build Docs ✓ (skip path, 12 s). mergeable_state unknown at read (draft; base origin/main has moved to f0e72e26, 7 commits ahead of merge-base c4326fe).

Maintainer-only merge: no — non-governed additive export under a maintainer-ruled programme; every open item above is implementer-floor or PM-seat process.

Side effects of this review, disclosed: none on any checkout. The head was read from a throwaway ref (deleted after posting); the build ran on a git archive extraction in scratch with the sibling objectui-7058-main install borrowed read-only via symlinks — vitest touched that install's ignored node_modules/.vite/vitest cache and left .vite-temp empty; its tracked tree is clean.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 8, 2026 21:28
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit dacb402 Sep 8, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8345-strict-authoring-twin branch September 8, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants